﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.IO;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Host;
using System.Threading;
using System.Management.Automation;
using System.Collections.ObjectModel;
using System.Security;
using System.Text.RegularExpressions;

namespace Decav.Web.PowerShell
{
    /// <summary>
    /// Handles requests to pages that are served by a PowerShell script.
    /// </summary>
    public class PowerShellHttpHandler : IHttpHandler
    {
        private static Regex _outputBlockParser = new Regex("<%=(?<script>.+?)%>", RegexOptions.Singleline);
        private static Regex _literalParser = new Regex("(^|%>)(?<literal>.+?)($|<%)", RegexOptions.Singleline);

        bool IHttpHandler.IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// Gets the path to the physical script that the handler
        /// will execute.
        /// </summary>
        public string ScriptPhysicalPath { get; private set; }

        /// <summary>
        /// The runspace that the script will execute in.
        /// </summary>
        protected Runspace PowerShellRunspace { get; private set; }

        /// <summary>
        /// Processes an incoming ASP.NET request.
        /// </summary>
        /// <param name="context">The context to process.</param>
        public void ProcessRequest(HttpContext context)
        {
            Initialize();
            try
            {
                this.PowerShellRunspace = CreateRunspace();
                using (MemoryStream poshScript = new MemoryStream())
                using (StreamReader poshInputStream = new StreamReader(this.ScriptPhysicalPath))
                {
                    StreamWriter poshScriptWriter = new StreamWriter(poshScript);
                    PrepareScript(poshInputStream, poshScriptWriter);
                    poshScriptWriter.Flush(); // NOTE: Cannot dispose or the underlying stream gets disposed!!!!

                    this.PowerShellRunspace.Open();
                    ConfigureRuntimeEnvironment();

                    poshScript.Seek(0, SeekOrigin.Begin);
                    ExecuteScript(new StreamReader(poshScript), HttpContext.Current.Response.Output);

                    this.PowerShellRunspace.Close();
                }
            }
            finally
            {
                ShutdownPowerShell();
            }
        }

        /// <summary>
        /// Initializes the handler.
        /// </summary>
        private void Initialize()
        {
            this.ScriptPhysicalPath = HttpContext.Current.Request.MapPath(HttpContext.Current.Request.Url.PathAndQuery.Split('?')[0]);

            if (!File.Exists(this.ScriptPhysicalPath))
            {
                throw new FileNotFoundException("The specified script could not be found.");
            }
        }

        /// <summary>
        /// Creates a new runspace.
        /// </summary>
        /// <returns>The runspace to execute in.</returns>
        protected virtual Runspace CreateRunspace()
        {
            return RunspaceFactory.CreateRunspace(new WebHttpRequestHost(HttpContext.Current));
        }

        /// <summary>
        /// Configures the runspace with global settings before
        /// an exectuon.
        /// </summary>
        protected virtual void ConfigureRuntimeEnvironment()
        {
            this.PowerShellRunspace.SessionStateProxy.SetVariable("PhysicalApplicationPath", HttpContext.Current.Request.PhysicalApplicationPath);
            this.PowerShellRunspace.SessionStateProxy.SetVariable("Request", HttpContext.Current.Request);
            this.PowerShellRunspace.SessionStateProxy.SetVariable("Response", HttpContext.Current.Response);
            this.PowerShellRunspace.SessionStateProxy.SetVariable("HttpContext", HttpContext.Current);

            using (Pipeline pipeline = this.PowerShellRunspace.CreatePipeline())
            {
                pipeline.Commands.AddScript("set-location $PhysicalApplicationPath", false);
                pipeline.Invoke();
            }
        }

        /// <summary>
        /// Parses the script in its raw form into a format that
        /// can be executed by the PowerShell environment.
        /// </summary>
        /// <remarks>
        /// Typically, preparing the script will involve converting
        /// any literals (code not contained in code blocks) into write-host
        /// operations so they can be read by the PowerShell runtime.
        /// </remarks>
        /// <param name="inputStream">The stream to the script</param>
        /// <param name="outputStream">The stream to output the revised script to.</param>
        protected virtual void PrepareScript(TextReader inputStream, TextWriter outputStream)
        {
            string input = inputStream.ReadToEnd();

            string output = _outputBlockParser.Replace(
                input,
                m =>
                {
                    return string.Format("<% {0} | write-host; %>", m.Groups["script"].Value);
                });

            output = _literalParser.Replace(
                output, 
                m =>
                {
                    return string.Format(";write-host \"{0}\" -noNewLine;", m.Groups["literal"].Value.Replace(@"""", @"`"""));
                });

            outputStream.Write(output);
        }

        /// <summary>
        /// Executes a script against the runspace.
        /// </summary>
        /// <param name="inputScript">The script to execute</param>
        /// <param name="output">The text writer to write results to.</param>
        private void ExecuteScript(StreamReader inputScript, TextWriter output)
        {
            using (Pipeline pipeline = this.PowerShellRunspace.CreatePipeline())
            {
                pipeline.Commands.AddScript(inputScript.ReadToEnd(), true);
                pipeline.Invoke();
            }
        }

        /// <summary>
        /// Shuts down the PowerShell runtime.
        /// </summary>
        private void ShutdownPowerShell()
        {
            if (this.PowerShellRunspace != null)
                this.PowerShellRunspace.Dispose();
        }
    }
}
